/*
**************************************************************************************
*Myriad Engine                                                                       *
*Copyright (C) 2006-2007, 5d Studios (www.5d-Studios.com)                            *
*                                                                                    *
*This library is free software; you can redistribute it and/or                       *
*modify it under the terms of the GNU Lesser General Public                          *
*License as published by the Free Software Foundation; either                        *
*version 2.1 of the License, or (at your option) any later version.                  *
*                                                                                    *
*This library is distributed in the hope that it will be useful,                     *
*but WITHOUT ANY WARRANTY; without even the implied warranty of                      *
*MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU                   *
*Lesser General Public License for more details.                                     *
*                                                                                    *
*You should have received a copy of the GNU Lesser General Public                    *
*License along with this library; if not, write to the Free Software                 *
*Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA      *
**************************************************************************************
*/
package leola.backend.interpreter;

import java.lang.reflect.Constructor;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import leola.ast.ClassDeclStmt;
import leola.ast.Stmt;
import leola.backend.EvalException;
import leola.types.LeoClass;
import leola.types.LeoNativeClass;
import leola.types.LeoNull;
import leola.types.LeoObject;

/**
 * Stores the Class Definitions
 * 
 * @author Tony
 *
 */
public class ClassDefinitions {
	/**
	 * The interpreter
	 */
	private Interpreter interpreter;

	/**
	 * Native stored classes
	 */
	private Set<String> storedClasses;
	
	/**
	 * defined Leola classes
	 */
	private Map<String, ClassDeclStmt> definedLeoClasses;
	
	/**
	 * @param interpreter
	 */
	public ClassDefinitions(Interpreter interpreter) {
		this.interpreter = interpreter;
		this.storedClasses = new HashSet<String>();
		this.definedLeoClasses = new ConcurrentHashMap<String, ClassDeclStmt>();
	}
	

	/**
	 * Store a defined class
	 * @param classStmt
	 */
	public void store(ClassDeclStmt classStmt) {
		this.definedLeoClasses.put(classStmt.getClassName(), classStmt);
	}
	
	/**
	 * Determines if the class is defined.
	 * 
	 * @param className
	 * @return
	 */
	public boolean isClass(String className) {
		return this.definedLeoClasses.containsKey(className);
	}
	
	/**
	 * Instantiates a new instance of a class.
	 * 
	 * @param className
	 * @param params
	 * @return
	 */
	public LeoClass newInstance(String className, LeoObject ... params) throws Exception {
		if (! isClass(className) ) {
			throw new EvalException("The supplied className does not exist in the definitions: " + className);
		}
		
		ClassDeclStmt classStmt = this.definedLeoClasses.get(className);
		
		Scope classScope = new Scope(true, className, false);
		SymbolTable table = classScope.getSymbolTable();
		
		String[] classParams = classStmt.getClassParameters();
		if ( classParams != null ) {
			for(int i = 0; i < classParams.length; i++) {
				String paramName = classParams[i];
				
				LeoObject value = LeoNull.LEONULL;
				if ( i < params.length) {
					value = params[i];
				}
				
				table.put(paramName, value);
			}			
		}
		
		LeoObject superClass = constructSuperClass(table, classStmt);
									
		LeoClass newClass = new LeoClass(classScope, className, classStmt.getClassBodyStmt(), superClass);
		
		SymbolStack symbolStack = this.interpreter.getSymbolStack();
		symbolStack.push(newClass.getScope());
		{
			Stmt classBody = newClass.getClassBody();			
			classBody.visit(this.interpreter);
		}
		symbolStack.pop();
		
		return newClass;		
	}
	
	/**
	 * Constructs the super class.
	 * 
	 * @param table
	 * @param classStmt
	 * @return
	 * @throws Exception
	 */
	private LeoObject constructSuperClass(SymbolTable table, ClassDeclStmt classStmt) throws Exception {
		LeoObject superClass = null;
		
		/* parse the parent */
		String parentClassName = classStmt.getParentClassName();
		if ( parentClassName != null ) {
			String parentParams[] = classStmt.getParentClassParams();
			LeoObject[] superParams = new LeoObject[parentParams.length];
			for(int i = 0; i < parentParams.length; i++ ) {
				String paramName = parentParams[i];
				LeoObject obj = table.get(paramName);
				if (obj == null ) {
					throw new EvalException("Variable name undefined for: " + paramName);
				}
				
				superParams[i] = obj;
			}
			
			if ( isNativeClass(parentClassName) ) {
				superClass = newNativeInstance(parentClassName, superParams);
			}
			else if ( isClass(parentClassName) ) {
				superClass = newInstance(parentClassName, superParams);
			}
			else {
				throw new EvalException("No super class defined for: " + parentClassName);
			}
		}	
		
		return superClass;
	}
	
	/**
	 * Determines if the supplied class name is a native class.
	 * 
	 * @param className
	 * @return
	 */
	public boolean isNativeClass(String className) {
		boolean result = false;
		try {
			Class.forName(className);
			result = true;
		}
		catch(Throwable e) {}
		
		return result;
	}
	
	/**
	 * Creates a new Native class.
	 * 
	 * @param className
	 * @param params
	 * @return
	 * @throws Exception
	 */
	public LeoNativeClass newNativeInstance(String className, LeoObject ... params) throws Exception {
		LeoNativeClass result = null;
				
		try {
			
			Class<?> nativeClass = Class.forName(className);
			Object instance = null;
			for(Constructor<?> constructor : nativeClass.getConstructors()) {
				Class<?>[] paramTypes = constructor.getParameterTypes();
				if ( paramTypes.length == params.length) {
					instance = tryNativeConstructor(constructor, params, paramTypes);
					if ( instance != null ) {
						break;
					}
				}
			}
			
			if ( instance != null ) {
				result = new LeoNativeClass(nativeClass, instance);
				
				/* if we have not stored this class yet, do so now */
				if ( ! this.storedClasses.contains(className)) {
					this.storedClasses.add(className);
					this.interpreter.loadClass(className);
				}
			}
			
		}
		catch(Throwable e) {
			throw new EvalException("Unable to construct native type: " + className);
		}
		
		if (result == null ) {
			throw new EvalException("Unable to construct native type: " + className);
		}
		
		return result;
	}
	
	/**
	 * Attempts to instantiate the object
	 * @param constructor
	 * @param params
	 * @param paramTypes
	 * @return
	 */
	private Object tryNativeConstructor(Constructor<?> constructor, LeoObject[] params, Class<?>[] paramTypes) {
		Object result = null;
		try {
			ObjectArrayJavaTypeVisitor visitor = new ObjectArrayJavaTypeVisitor();
			Object[] args = new Object[paramTypes.length];
			visitor.params = args;
			
			for(int i = 0; i < paramTypes.length; i++ ) {
				visitor.currentIndex = i;
				
				Class<?> aCl = paramTypes[i];
				LeoTypeConverter.convertLeoObjectToJavaObj(visitor, aCl, params[i]);
			}
			
			result = constructor.newInstance(args);
		}
		catch(Throwable e) {
		}
		
		return (result);
	}
}
